Skip to content

feat: support isolated scope#2404

Merged
kazupon merged 1 commit into
masterfrom
feat/2207
Feb 25, 2026
Merged

feat: support isolated scope#2404
kazupon merged 1 commit into
masterfrom
feat/2207

Conversation

@kazupon
Copy link
Copy Markdown
Member

@kazupon kazupon commented Feb 25, 2026

resolve #2207

related #2098

Summary by CodeRabbit

  • New Features

    • Added isolated scope support enabling independent Composer instances that don't attach to component-local scopes, with fallback to global scope for missing keys and optional locale inheritance control.
  • Documentation

    • Documented isolated scope feature with usage examples and configuration options across English, Japanese, and Chinese guides.
  • Tests

    • Added comprehensive test coverage for isolated scope functionality.

@kazupon kazupon added the Type: Feature Includes new features label Feb 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Introduces an 'isolated' scope option to useI18n, enabling composables to create independent Composer instances separate from component local scopes, preventing message key conflicts. Corresponding documentation is added in three languages, core types and logic are updated, and comprehensive tests are included.

Changes

Cohort / File(s) Summary
Documentation
docs/guide/advanced/composition.md, docs/jp/guide/advanced/composition.md, docs/zh/guide/advanced/composition.md
Added "Isolated/Separated Scope" sections explaining isolated Composer instances, their characteristics (not component-bound, independent lifecycle, fallback to global), usage examples in composables and components, and inheritance behavior with inheritLocale option.
Type Definitions
packages/vue-i18n-core/src/components/base.ts
Updated ComponentI18nScope type to exclude both 'local' and 'isolated' scopes instead of only 'local', with corresponding validator and default value adjustments.
Core Implementation
packages/vue-i18n-core/src/i18n.ts
Extended I18nScope type to include 'isolated' option and implemented full isolated scope support in useI18n: independent Composer instance creation with detached UID, root fallback propagation, lifecycle management via onScopeDispose, and DevTools integration.
Test Suite
packages/vue-i18n-core/test/i18n.test.ts
Added comprehensive test cases for isolated scope covering basic usage, multiple isolated scopes, coexistence with local/global scopes, locale inheritance from global instance, and fallback behavior for missing keys.

Sequence Diagram

sequenceDiagram
    participant Component
    participant Composable
    participant IsolatedComposer as Isolated Composer
    participant GlobalComposer as Global Composer
    
    Component->>Component: mount (has local scope)
    Composable->>IsolatedComposer: useI18n({ useScope: 'isolated', messages: {...} })
    IsolatedComposer->>IsolatedComposer: create independent instance<br/>(not linked to component UID)
    IsolatedComposer->>GlobalComposer: inherit locale
    Component->>IsolatedComposer: t('key.in.composable')
    IsolatedComposer-->>Component: return translation
    alt key missing in isolated
        IsolatedComposer->>GlobalComposer: fallback to root/global
        GlobalComposer-->>IsolatedComposer: return global translation
    end
    Component->>Component: unmount
    IsolatedComposer->>IsolatedComposer: cleanup via onScopeDispose
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

Type: Documentation

Poem

🐰 Hop hop, a scope so isolated,
Where composables dance, conflicted no more!
Messages clash not in tangled creation,
Each Composer sings its own translation lore. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support isolated scope' accurately describes the main feature addition, which is implementing isolated scope functionality in the useI18n API.
Linked Issues check ✅ Passed The PR fully implements the requested feature from issue #2207: enabling composables to use their own isolated i18n scope with local messages independent of component scope.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the isolated scope feature: documentation, type definitions, implementation in useI18n, component restrictions, and comprehensive tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/2207

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying vue-i18n-next with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0c291d3
Status: ✅  Deploy successful!
Preview URL: https://8d0c6f08.vue-i18n-next.pages.dev
Branch Preview URL: https://feat-2207.vue-i18n-next.pages.dev

View logs

@github-actions
Copy link
Copy Markdown

Size Report

Bundles

File Size Gzip Brotli
core.esm-browser.prod.js 44.09 kB 12.69 kB 11.37 kB
core.global.prod.js 35.10 kB 11.74 kB 10.54 kB
core.runtime.esm-browser.prod.js 26.80 kB 8.31 kB 7.45 kB
core.runtime.global.prod.js 20.23 kB 7.66 kB 6.89 kB
message-compiler.esm-browser.prod.js 23.42 kB 6.50 kB 5.83 kB
message-compiler.global.prod.js 20.66 kB 6.26 kB 5.62 kB
petite-vue-i18n-core.esm-browser.prod.js 23.37 kB (+0.27 kB) 7.61 kB (+0.05 kB) 6.82 kB (+0.04 kB)
petite-vue-i18n-core.global.prod.js 17.77 kB (+0.21 kB) 6.75 kB (+0.06 kB) 6.08 kB (+0.04 kB)
petite-vue-i18n.esm-browser.prod.js 43.19 kB (+0.27 kB) 12.58 kB (+0.05 kB) 11.26 kB (+0.03 kB)
petite-vue-i18n.global.prod.js 34.17 kB (+0.22 kB) 11.28 kB (+0.05 kB) 10.16 kB (+0.04 kB)
petite-vue-i18n.runtime.esm-browser.prod.js 25.76 kB (+0.27 kB) 8.10 kB (+0.05 kB) 7.31 kB (+0.05 kB)
petite-vue-i18n.runtime.global.prod.js 19.29 kB (+0.21 kB) 7.18 kB (+0.06 kB) 6.48 kB (+0.03 kB)
vue-i18n.esm-browser.prod.js 56.81 kB (+0.27 kB) 16.25 kB (+0.05 kB) 14.56 kB (+0.03 kB)
vue-i18n.global.prod.js 44.80 kB (+0.22 kB) 14.64 kB (+0.05 kB) 13.19 kB (+0.04 kB)
vue-i18n.runtime.esm-browser.prod.js 39.36 kB (+0.27 kB) 11.77 kB (+0.05 kB) 10.64 kB (+0.04 kB)
vue-i18n.runtime.global.prod.js 29.92 kB (+0.22 kB) 10.58 kB (+0.05 kB) 9.58 kB (+0.06 kB)

Usages

Name Size Gzip Brotli
packages/size-check-core (@intlify/core) 12.00 kB 4.77 kB 4.29 kB
packages/size-check-petite-vue-i18n (petite-vue-i18n) 84.29 kB (+0.19 kB) 32.27 kB (+0.06 kB) 29.10 kB (+0.06 kB)
packages/size-check-vue-i18n (vue-i18n) 88.98 kB (+0.19 kB) 33.58 kB (+0.06 kB) 30.29 kB (+0.11 kB)

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Feb 25, 2026

Open in StackBlitz

@intlify/core

npm i https://pkg.pr.new/@intlify/core@2404

@intlify/core-base

npm i https://pkg.pr.new/@intlify/core-base@2404

@intlify/devtools-types

npm i https://pkg.pr.new/@intlify/devtools-types@2404

@intlify/message-compiler

npm i https://pkg.pr.new/@intlify/message-compiler@2404

petite-vue-i18n

npm i https://pkg.pr.new/petite-vue-i18n@2404

@intlify/shared

npm i https://pkg.pr.new/@intlify/shared@2404

vue-i18n

npm i https://pkg.pr.new/vue-i18n@2404

@intlify/vue-i18n-core

npm i https://pkg.pr.new/@intlify/vue-i18n-core@2404

commit: 0c291d3

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/vue-i18n-core/src/i18n.ts (1)

566-610: Consider extracting shared composer lifecycle wiring for isolated and local paths.

Lines 570–607 duplicate composer extension, devtools emitter setup, and disposal logic also present in the local branch, which increases drift risk.

♻️ Suggested refactor sketch
+function setupComposerLifecycle(
+  i18nInternal: I18nInternal,
+  composer: Composer
+): void {
+  if (i18nInternal.__composerExtend) {
+    ;(composer as any)[DisposeSymbol] = i18nInternal.__composerExtend(composer)
+  }
+  let emitter: VueDevToolsEmitter | null = null
+  if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
+    emitter = createEmitter<VueDevToolsEmitterEvents>()
+    const _composer = composer as any
+    _composer[EnableEmitter]?.(emitter)
+    emitter.on('*', addTimelineEvent)
+  }
+  const currentScope = getCurrentScope()
+  if (currentScope) {
+    onScopeDispose(() => {
+      if ((__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__) {
+        emitter?.off('*', addTimelineEvent)
+        const _composer = composer as any
+        _composer[DisableEmitter]?.()
+      }
+      const dispose = (composer as any)[DisposeSymbol]
+      if (dispose) {
+        dispose()
+        delete (composer as any)[DisposeSymbol]
+      }
+    })
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-i18n-core/src/i18n.ts` around lines 566 - 610, The isolated
branch duplicates lifecycle/devtools wiring (composer extension, devtools
emitter setup, and onScopeDispose teardown) that also exists in the local
branch; extract that shared logic into a helper (e.g., wireComposerLifecycle or
setupComposerLifecycle) and call it from both paths. The helper should accept
the created composer and I18nInternal (to run __composerExtend and store
DisposeSymbol), create and enable the devtools emitter (using createEmitter,
EnableEmitter/DisableEmitter, and emitter.on/off with addTimelineEvent) only
when (__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) && !__NODE_JS__, and register
the same onScopeDispose teardown to remove the emitter listener, disable
emitter, and call/delete the DisposeSymbol; replace the duplicated blocks in
createComposer call sites (where composer is created and where DisposeSymbol,
EnableEmitter, DisableEmitter, emitter, __composerExtend, and onScopeDispose are
referenced) with a call to this helper.
packages/vue-i18n-core/test/i18n.test.ts (1)

463-503: Add a same-key collision regression test for local vs isolated scopes.

The current test (lines 463-503) uses different keys across scopes (hi in local, status in isolated). Add a test where both scopes use the same key to verify that isolated scope properly isolates from local scope when keys collide—this closes a regression gap.

Suggested test case
+    test('isolated scope does not collide with local scope on same key', async () => {
+      const i18n = createI18n({
+        locale: 'en',
+        messages: { en: { status: 'from global' } }
+      })
+
+      let localComposer: Composer
+      let isolatedComposer: Composer
+      const App = defineComponent({
+        setup() {
+          localComposer = useI18n({
+            messages: { en: { status: 'from local' } }
+          }) as Composer
+          isolatedComposer = useI18n({
+            useScope: 'isolated',
+            messages: { en: { status: 'from isolated' } }
+          }) as Composer
+          return {}
+        },
+        template: `<p>foo</p>`
+      })
+      await mount(App, i18n)
+
+      expect(localComposer.t('status')).toEqual('from local')
+      expect(isolatedComposer.t('status')).toEqual('from isolated')
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-i18n-core/test/i18n.test.ts` around lines 463 - 503, Add a
regression test that verifies isolated composable scopes truly isolate when keys
collide with local/component scope: duplicate the existing "coexists with local
scope" test pattern but use the same key name in the component local useI18n and
in the isolated composable useI18n (e.g., both use 'status' or 'hi') and assert
the component Composer.t('status') (or 'hi') returns the component message while
the composableResult.status returns the isolated message; reference the same
functions and symbols (createI18n, useI18n with useScope: 'isolated',
useMyComposable, localComposer variable, composableResult, Composer, and App) so
the new test sits alongside the existing one and ensures collision isolation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/guide/advanced/composition.md`:
- Around line 471-535: Update the wording in the isolated scope tip to say the
isolated scope inherits locale from the parent/global scope (not just “global
scope”) to match runtime behavior (see composerOptions.__root set to
parentComposer || gl in the core); change the sentence in the EN isolated scope
section (the NOTE tip) to mention “parent/global scope” and also audit and
update the JP and ZH translations of the same tip to the equivalent
parent/global phrasing so all locales are consistent with useI18n and Composer
behavior.

---

Nitpick comments:
In `@packages/vue-i18n-core/src/i18n.ts`:
- Around line 566-610: The isolated branch duplicates lifecycle/devtools wiring
(composer extension, devtools emitter setup, and onScopeDispose teardown) that
also exists in the local branch; extract that shared logic into a helper (e.g.,
wireComposerLifecycle or setupComposerLifecycle) and call it from both paths.
The helper should accept the created composer and I18nInternal (to run
__composerExtend and store DisposeSymbol), create and enable the devtools
emitter (using createEmitter, EnableEmitter/DisableEmitter, and emitter.on/off
with addTimelineEvent) only when (__DEV__ || __FEATURE_PROD_VUE_DEVTOOLS__) &&
!__NODE_JS__, and register the same onScopeDispose teardown to remove the
emitter listener, disable emitter, and call/delete the DisposeSymbol; replace
the duplicated blocks in createComposer call sites (where composer is created
and where DisposeSymbol, EnableEmitter, DisableEmitter, emitter,
__composerExtend, and onScopeDispose are referenced) with a call to this helper.

In `@packages/vue-i18n-core/test/i18n.test.ts`:
- Around line 463-503: Add a regression test that verifies isolated composable
scopes truly isolate when keys collide with local/component scope: duplicate the
existing "coexists with local scope" test pattern but use the same key name in
the component local useI18n and in the isolated composable useI18n (e.g., both
use 'status' or 'hi') and assert the component Composer.t('status') (or 'hi')
returns the component message while the composableResult.status returns the
isolated message; reference the same functions and symbols (createI18n, useI18n
with useScope: 'isolated', useMyComposable, localComposer variable,
composableResult, Composer, and App) so the new test sits alongside the existing
one and ensures collision isolation.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8039e71 and 0c291d3.

📒 Files selected for processing (6)
  • docs/guide/advanced/composition.md
  • docs/jp/guide/advanced/composition.md
  • docs/zh/guide/advanced/composition.md
  • packages/vue-i18n-core/src/components/base.ts
  • packages/vue-i18n-core/src/i18n.ts
  • packages/vue-i18n-core/test/i18n.test.ts

Comment on lines +471 to +535
## Isolated scope

The isolated scope creates an independent Composer instance that is **not tied to the component**. This is useful when you want to use `useI18n` inside a composable with its own translation messages, without conflicting with the component's local scope.

### Why isolated scope?

When a component and a composable both call `useI18n` with local scope, the second call conflicts with the first because only one local scope per component is allowed. The isolated scope solves this by creating a Composer that:

- Is **not registered** with the component's uid (no duplicate detection)
- Does **not propagate** to child components via `provide`
- Does **not merge** SFC i18n custom blocks
- **Inherits locale** from the parent/global scope (by default)
- **Falls back** to the parent/global scope for missing translation keys

### Usage in composables

<!-- eslint-skip -->

```ts
// useProjectStatus.ts
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

export function useProjectStatus(project) {
const { t } = useI18n({
useScope: 'isolated',
messages: {
en: { active: 'Active', inactive: 'Inactive' },
ja: { active: '稼働中', inactive: '停止中' }
}
})

return computed(() => project.isActive ? t('active') : t('inactive'))
}
```

<!-- eslint-skip -->

```vue
<!-- MyComponent.vue -->
<script setup>
import { useI18n } from 'vue-i18n'
import { useProjectStatus } from './useProjectStatus'

// Local scope for the component
const { t } = useI18n({
messages: {
en: { title: 'Project Dashboard' },
ja: { title: 'プロジェクトダッシュボード' }
}
})

// Composable with isolated scope — no conflict!
const status = useProjectStatus(project)
</script>

<template>
<h1>{{ t('title') }}</h1>
<p>{{ status }}</p>
</template>
```

:::tip NOTE
The isolated scope inherits locale from the global scope by default. When the global locale changes, the isolated scope's locale is automatically updated. You can disable this behavior by setting `inheritLocale: false`.
:::
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify doc wording vs runtime behavior for isolated scope inheritance.

echo "=== Checking locale inheritance wording in documentation ==="
rg -n "Inherits locale|inherits locale|parent/global scope|global scope by default" \
  docs/guide/advanced/composition.md \
  docs/jp/guide/advanced/composition.md \
  docs/zh/guide/advanced/composition.md 2>/dev/null || echo "Documentation files not found, checking available files..."

echo -e "\n=== Checking for locale inheritance patterns ==="
rg -n "locale.*inherit|inherit.*locale" docs/guide/advanced/composition.md -A2 -B2 2>/dev/null || echo "Pattern not found"

echo -e "\n=== Checking runtime implementation ==="
rg -n "parentComposer.*gl|__root.*parent" packages/vue-i18n-core/src/i18n.ts -B3 -A3 2>/dev/null || echo "Runtime code not found"

Repository: intlify/vue-i18n

Length of output: 2113


Fix locale inheritance wording inconsistency in the isolated scope section.

Line 482 correctly describes "inherits locale from the parent/global scope," but line 534 states only "global scope." The runtime implementation (composerOptions.__root = parentComposer || gl, confirmed at lines 574 and 643 in packages/vue-i18n-core/src/i18n.ts) uses the parent Composer first, then falls back to global scope—matching the parent/global behavior. Line 534 should be updated to reflect this:

📝 Suggested wording update (EN)
-The isolated scope inherits locale from the global scope by default. When the global locale changes, the isolated scope's locale is automatically updated. You can disable this behavior by setting `inheritLocale: false`.
+The isolated scope inherits locale from the parent scope (if available), otherwise from the global scope by default. When the inherited locale source changes, the isolated scope's locale is automatically updated. You can disable this behavior by setting `inheritLocale: false`.

Also update the JP/ZH documentation sections if they contain similar locale inheritance descriptions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/guide/advanced/composition.md` around lines 471 - 535, Update the
wording in the isolated scope tip to say the isolated scope inherits locale from
the parent/global scope (not just “global scope”) to match runtime behavior (see
composerOptions.__root set to parentComposer || gl in the core); change the
sentence in the EN isolated scope section (the NOTE tip) to mention
“parent/global scope” and also audit and update the JP and ZH translations of
the same tip to the equivalent parent/global phrasing so all locales are
consistent with useI18n and Composer behavior.

@kazupon kazupon merged commit d055524 into master Feb 25, 2026
36 checks passed
@kazupon kazupon deleted the feat/2207 branch February 25, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Feature Includes new features v11:backport

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use of local messages in composable

1 participant